/*
 *  Copyright 2009-2011 Stephen Colebourne
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package org.mookt.unit.currency;

import java.util.Collection;
import java.util.Collections;
import java.util.Currency;
import java.util.Set;

import org.mookt.unit.BaseUnit;
import org.mookt.unit.Dimension;
import org.mookt.unit.Unit;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;

/**
 * A unit of currency.
 * 
 * <p>
 * This class represents a unit of currency such as the British Pound, Euro or
 * US Dollar.
 * </p>
 * 
 * <p>
 * The set of loaded currencies is provided by a {@link Provider} instance. The
 * provider used is determined by the system property
 * {@code org.mookt.unit.currency.CurrencyUnit} which should be the fully
 * qualified class name of the provider. If no providers are defined, the
 * {@link ResourceFileProvider} will be loaded.
 * </p>
 * 
 * <p>
 * This class is immutable and thread-safe.
 * </p>
 * 
 * @author Stephen Colebourne
 * @author Ricardo Engel
 */
public final class CurrencyUnit implements Unit, Comparable<CurrencyUnit> {

    private static final long serialVersionUID = 1L;

    /**
     * The base dimension for all currencies. This allows currencies conversion,
     * as dimensionless units cannot be converted into one another.
     */
    private static final Dimension CURRENCY = Dimension.of('$');

    private static final Provider PROVIDER = configureProvider();

    // --------------------- CURRENCY CONSTANTS ------------------------------
    // a selection of commonly traded, stable currencies
    /** The currency 'USD' - United States Dollar. */
    public static final CurrencyUnit USD = get("USD");

    /** The currency 'EUR' - Euro. */
    public static final CurrencyUnit EUR = get("EUR");

    /** The currency 'JPY' - Japanese Yen. */
    public static final CurrencyUnit JPY = get("JPY");

    /** The currency 'GBP' - British pound. */
    public static final CurrencyUnit GBP = get("GBP");

    /** The currency 'CHF' - Swiss Franc. */
    public static final CurrencyUnit CHF = get("CHF");

    /** The currency 'AUD' - Australian Dollar. */
    public static final CurrencyUnit AUD = get("AUD");

    /** The currency 'CAD' - Canadian Dollar. */
    public static final CurrencyUnit CAD = get("CAD");

    // --------------------- PRIVATE FIELDS ----------------------------------
    /** Delegate currencies symbol and dimension to a {@link BaseUnit} instance. */
    private final BaseUnit baseUnit;

    /** The number of decimal places for this currency. */
    private final byte decimalPlaces;

    /** The currency code, not null. */
    private final String code;

    /** The numeric currency code. */
    private final short numericCode;

    /** The set of country codes, not null. */
    private final Set<String> countryCodes;

    // ----------------------- CONSTRUCTOR ----------------------------------
    /**
     * Constructor, creating a new currency instance.
     * 
     * @param symbol the currency symbol, not null
     * @param decimalPlaces the decimal places, between 0 and 9
     * @param roundingMode the rounding behaviour, not null
     * @param code the currency code, not null
     * @param numericCode the numeric currency code, between 0 and 999
     * @param countryCodes the list of country codes, not null
     */
    private CurrencyUnit(final String symbol, final byte decimalPlaces,
            final String code, final short numericCode,
            final Set<String> countryCodes) {
        Preconditions.checkNotNull(code, "Currency code must not be null");
        Preconditions.checkArgument(numericCode >= 0 && numericCode <= 999,
                "Invalid numeric code");
        Preconditions.checkArgument(decimalPlaces >= 0 && decimalPlaces <= 9,
                "Invalid number of decimal places");
        Preconditions.checkNotNull(countryCodes,
                "Country codes must not be null");

        baseUnit = BaseUnit.of(symbol, CURRENCY);
        this.decimalPlaces = decimalPlaces;
        this.code = code;
        this.numericCode = numericCode;
        this.countryCodes = countryCodes;
    }

    /**
     * Loads a provider determined by the system property configuration as the
     * standard provider for currency instances.
     * 
     * <pre>
     * -Dorg.mookt.unit.currency.CurrencyUnitProvider=<class path>
     * </pre>
     * 
     * <p>
     * Although it is not enforced, it is recommended that the provider is
     * deterministic on the list of currencies provided, i.e. the list is not
     * modified over time.
     * </p>
     */
    private static Provider configureProvider() {
        try {
            Provider loader;
            try {
                final String clsName = System.getProperty(
                        Provider.class.getName(),
                        ResourceFileProvider.class.getName());
                final Class<? extends Provider> cls = CurrencyUnit.class
                        .getClassLoader().loadClass(clsName)
                        .asSubclass(Provider.class);
                loader = cls.newInstance();
            } catch (final SecurityException ex) {
                loader = new ResourceFileProvider();
            }
            return loader;
        } catch (final RuntimeException ex) {
            throw ex;
        } catch (final Exception ex) {
            throw new RuntimeException(ex.toString(), ex);
        }
    }

    // -----------------------------------------------------------------------
    /**
     * Obtains an instance of {@code CurrencyUnit} matching the specified JDK
     * currency.
     * 
     * <p>
     * This converts the JDK currency instance to a currency unit using the
     * code.
     * </p>
     * 
     * @param currency the currency, not null
     * @return the singleton instance, never null
     */
    public static CurrencyUnit fromCurrency(final Currency currency) {
        Preconditions.checkNotNull(currency, "Currency must not be null");
        return get(currency.getCurrencyCode());
    }

    /**
     * Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217
     * three letter currency code.
     * 
     * <p>
     * A currency is uniquely identified by ISO-4217 three letter code.
     * </p>
     * 
     * @param code the currency code, not null
     * @return the singleton instance, never null
     * @throws IllegalCurrencyException if the currency is unknown
     */
    public static CurrencyUnit get(final String code) {
        Preconditions.checkNotNull(code, "Currency code must not be null");
        final CurrencyUnit currency = PROVIDER.get(code);
        if (currency == null) {
            throw new IllegalArgumentException("Unknown currency '" + code
                    + '\'');
        }
        return currency;
    }

    /**
     * Obtains an instance of {@code CurrencyUnit} for the specified ISO-4217
     * numeric currency code.
     * <p>
     * The numeric code is an alternative to the three letter code.
     * 
     * @param numericCode the numeric currency code, not null
     * @return the singleton instance, never null
     * @throws IllegalCurrencyException if the currency is unknown
     */
    public static CurrencyUnit get(final short numericCode) {
        final CurrencyUnit currency = PROVIDER.get(numericCode);
        if (currency == null) {
            throw new IllegalArgumentException("Unknown currency '"
                    + numericCode + '\'');
        }
        return currency;
    }

    /**
     * Gets the list of all available currencies.
     * 
     * <p>
     * This class only permits known currencies to be returned, thus this list
     * is the complete list of valid singleton currencies. The list may change
     * after application startup, however this isn't recommended.
     * </p>
     * 
     * @return the sorted, independent, list of all registered currencies, never
     *         null
     */
    public static Collection<CurrencyUnit> getAll() {
        return PROVIDER.getAll();
    }

    // ------------------------- PUBLIC METHODS ------------------------------
    /**
     * Gets the number of decimal places typically used by this currency.
     * <p>
     * Different currencies have different numbers of decimal places by default.
     * For example, 'GBP' has 2 decimal places, but 'JPY' has zero.
     * </p>
     * 
     * @return the decimal places, from 0 to 9 (normally 0, 2 or 3)
     */
    public byte getDecimalPlaces() {
        return decimalPlaces;
    }

    /**
     * Gets the ISO-4217 three letter currency code.
     * 
     * <p>
     * Each currency is uniquely identified by a three-letter code.
     * </p>
     * 
     * @return the three letter ISO-4217 currency code, never null
     */
    public String getCode() {
        return code;
    }

    /**
     * Gets the ISO-4217 numeric currency code.
     * 
     * <p>
     * The numeric code is an alternative to the standard three letter code.
     * </p>
     * 
     * @return the numeric currency code
     */
    public int getNumericCode() {
        return numericCode;
    }

    /**
     * Gets internationally accepted symbol associated with this currency, such
     * as "$" for American dollars or "�" for British pounds.
     * 
     * <p>
     * If a currency doesn't have a proper symbol associated with it, the <a
     * href="http://en.wikipedia.org/wiki/Currency_(typography)">generic
     * currency sign</a> is returned.
     * </p>
     * 
     * @return the currency symbol, never null
     */
    public String getSymbol() {
        return baseUnit.getSymbol();
    }

    /**
     * Gets the group of countries which circulate this currency (not
     * necessarily officially).
     * 
     * <p>
     * The returned set is immutable. The list may change after application
     * startup, however this isn't recommended.
     * </p>
     * 
     * @return
     */
    public Set<String> getContryCodes() {
        return Collections.unmodifiableSet(countryCodes);
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Unit#getDimension()
     */
    public Dimension getDimension() {
        return baseUnit.getDimension();
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.mookt.unit.Unit#compatibleWith(org.mookt.unit.Unit)
     */
    public boolean compatibleWith(final Unit unit) {
        return baseUnit.compatibleWith(unit);
    }

    /**
     * Gets the JDK currency instance equivalent to this currency.
     * <p>
     * This attempts to convert a {@code CurrencyUnit} to a JDK {@code Currency}.
     * 
     * @return the JDK currency instance, never null
     * @throws IllegalArgumentException if no matching currency exists in the
     *         JDK
     */
    public Currency toCurrency() {
        return Currency.getInstance(code);
    }

    // --------------------- STANDARD JAVA METHODS ---------------------
    /**
     * Compares this currency to another by alphabetical comparison of the code.
     * 
     * @param other the other currency, not null
     * @return negative if earlier alphabetically, 0 if equal, positive if
     *         greater alphabetically
     */
    public int compareTo(final CurrencyUnit other) {
        Preconditions.checkNotNull(other,
                "Currency being compared cannot be null");
        return code.compareTo(other.code);
    }

    /**
     * Checks if this currency equals another currency.
     * <p>
     * The comparison checks only the 3 letter currency code.
     * 
     * @param obj the other currency, null returns false
     * @return true if equal
     */
    @Override
    public boolean equals(final Object obj) {
        if (obj instanceof CurrencyUnit) {
            final CurrencyUnit that = (CurrencyUnit) obj;
            return Objects.equal(code, that.code);
        } else {
            return false;
        }
    }

    /**
     * Returns a suitable hash code for the currency based on the currency code.
     * 
     * @return the hash code
     */
    @Override
    public int hashCode() {
        return Objects.hashCode(code);
    }

    /**
     * Gets the currency code as a string.
     * 
     * @return the currency code, never null
     */
    @Override
    public String toString() {
        return code;
    }

    // --------------------- Currency Factory class ---------------------
    /**
     * Currency factory with a single static factory method to be used only by
     * {@link Provider} implementations to generate instances of
     * {@link CurrencyUnit}s provided by them.
     * 
     * @author Ricardo Engel
     */
    public static class Factory {

        /**
         * Creates a new {@link CurrencyUnit} instance with the given arguments.
         * 
         * @param symbol the currency symbol, not null
         * @param decimalPlaces the decimal places, between 0 and 9
         * @param roundingMode the rounding behaviour, not null
         * @param code the currency code, not null
         * @param numericCode the numeric currency code, between 0 and 999
         * @param countryCodes the list of country codes, not null
         */
        public static CurrencyUnit create(final String symbol,
                final byte decimalPlaces, final String code,
                final short numericCode, final Set<String> countryCodes) {
            return new CurrencyUnit(symbol, decimalPlaces, code, numericCode,
                    countryCodes);
        }
    }
}